From 33b683d037848fbc5f7b31fe5c9a3934373a58a7 Mon Sep 17 00:00:00 2001 From: Athou Date: Sat, 9 Aug 2014 15:25:41 +0200 Subject: [PATCH] session support --- gulpfile.js | 5 +- src/main/app/js/main.js | 10 +- src/main/app/templates/welcome.html | 3 + .../com/commafeed/CommaFeedApplication.java | 28 +++- .../backend/service/UserService.java | 15 +- .../frontend/auth/SecurityCheckProvider.java | 138 +++++++++++------- .../frontend/model/request/LoginRequest.java | 20 +++ .../commafeed/frontend/resource/UserREST.java | 17 +++ .../frontend/servlet/LogoutServlet.java | 25 ++++ .../frontend/servlet/NextUnreadServlet.java | 18 ++- 10 files changed, 202 insertions(+), 77 deletions(-) create mode 100644 src/main/app/templates/welcome.html create mode 100644 src/main/java/com/commafeed/frontend/model/request/LoginRequest.java create mode 100644 src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java diff --git a/gulpfile.js b/gulpfile.js index 2218e846..f6cb383b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -72,7 +72,10 @@ gulp.task('serve', function() { root : BUILD_DIR, port : 8082, middleware : function() { - return [modRewrite(['^/rest/(.*)$ http://localhost:8083/rest/$1 [P]'])]; + var rest = '^/rest/(.*)$ http://localhost:8083/rest/$1 [P]'; + var next = '^/next(.*)$ http://localhost:8083/next$1 [P]'; + var logout = '^/logout(.*)$ http://localhost:8083/logout$1 [P]'; + return [modRewrite([rest, next, logout])]; } }); }); diff --git a/src/main/app/js/main.js b/src/main/app/js/main.js index 05e6b3fd..3afde3d0 100644 --- a/src/main/app/js/main.js +++ b/src/main/app/js/main.js @@ -8,7 +8,7 @@ app.config(['$routeProvider', '$stateProvider', '$urlRouterProvider', '$httpProv cfpLoadingBarProvider.includeSpinner = false; $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|javascript):/); - var interceptor = ['$rootScope', '$q', function(scope, $q) { + var interceptor = ['$rootScope', '$q', '$injector', function(scope, $q, $injector) { var success = function(response) { return response; @@ -16,7 +16,7 @@ app.config(['$routeProvider', '$stateProvider', '$urlRouterProvider', '$httpProv var error = function(response) { var status = response.status; if (status == 401) { - window.location = 'logout'; + $injector.get('$state').transitionTo('welcome'); return; } else { return $q.reject(response); @@ -123,6 +123,12 @@ app.config(['$routeProvider', '$stateProvider', '$urlRouterProvider', '$httpProv templateUrl : 'templates/admin.metrics.html', controller : 'MetricsCtrl' }); + + $stateProvider.state('welcome', { + url : '/welcome', + templateUrl : 'templates/welcome.html', + controller : 'WelcomeCtrl' + }); $urlRouterProvider.when('/', '/feeds/view/category/all'); $urlRouterProvider.when('/admin', '/admin/settings'); diff --git a/src/main/app/templates/welcome.html b/src/main/app/templates/welcome.html new file mode 100644 index 00000000..1ee2e69a --- /dev/null +++ b/src/main/app/templates/welcome.html @@ -0,0 +1,3 @@ +
+
+
\ No newline at end of file diff --git a/src/main/java/com/commafeed/CommaFeedApplication.java b/src/main/java/com/commafeed/CommaFeedApplication.java index 0050ad69..93b562bb 100644 --- a/src/main/java/com/commafeed/CommaFeedApplication.java +++ b/src/main/java/com/commafeed/CommaFeedApplication.java @@ -4,6 +4,7 @@ import io.dropwizard.Application; import io.dropwizard.assets.AssetsBundle; import io.dropwizard.db.DataSourceFactory; import io.dropwizard.hibernate.HibernateBundle; +import io.dropwizard.jersey.sessions.HttpSessionProvider; import io.dropwizard.migrations.MigrationsBundle; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; @@ -12,6 +13,7 @@ import java.util.Date; import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.server.session.SessionHandler; import org.hibernate.SessionFactory; import com.codahale.metrics.MetricRegistry; @@ -63,6 +65,7 @@ import com.commafeed.backend.service.PubSubService; import com.commafeed.backend.service.StartupService; import com.commafeed.backend.service.UserService; import com.commafeed.frontend.auth.SecurityCheckProvider; +import com.commafeed.frontend.auth.SecurityCheckProvider.SecurityCheckUserServiceProvider; import com.commafeed.frontend.resource.AdminREST; import com.commafeed.frontend.resource.CategoryREST; import com.commafeed.frontend.resource.EntryREST; @@ -70,6 +73,7 @@ import com.commafeed.frontend.resource.FeedREST; import com.commafeed.frontend.resource.PubSubHubbubCallbackREST; import com.commafeed.frontend.resource.ServerREST; import com.commafeed.frontend.resource.UserREST; +import com.commafeed.frontend.servlet.LogoutServlet; import com.commafeed.frontend.servlet.NextUnreadServlet; @Slf4j @@ -78,6 +82,8 @@ public class CommaFeedApplication extends Application { public static final String USERNAME_ADMIN = "admin"; public static final String USERNAME_DEMO = "demo"; + public static final String SESSION_USER = "user"; + public static final Date STARTUP_TIME = new Date(); private HibernateBundle hibernateBundle; @@ -115,6 +121,7 @@ public class CommaFeedApplication extends Application { : new RedisCacheService(); log.info("using cache {}", cacheService.getClass()); + // DAOs FeedCategoryDAO feedCategoryDAO = new FeedCategoryDAO(sessionFactory); FeedDAO feedDAO = new FeedDAO(sessionFactory); FeedEntryContentDAO feedEntryContentDAO = new FeedEntryContentDAO(sessionFactory); @@ -128,6 +135,7 @@ public class CommaFeedApplication extends Application { FeedQueues queues = new FeedQueues(feedDAO, config, metrics); + // Services ApplicationPropertiesService applicationPropertiesService = new ApplicationPropertiesService(); DatabaseCleaningService cleaningService = new DatabaseCleaningService(feedDAO, feedEntryDAO, feedEntryContentDAO, feedEntryStatusDAO, feedSubscriptionDAO); @@ -144,22 +152,28 @@ public class CommaFeedApplication extends Application { config); StartupService startupService = new StartupService(sessionFactory, userDAO, userService); + // OPML OPMLImporter opmlImporter = new OPMLImporter(feedCategoryDAO, feedSubscriptionService, cacheService); OPMLExporter opmlExporter = new OPMLExporter(feedCategoryDAO, feedSubscriptionDAO); + // Feed fetching/parsing HttpGetter httpGetter = new HttpGetter(); FeedParser feedParser = new FeedParser(); FaviconFetcher faviconFetcher = new FaviconFetcher(httpGetter); - FeedFetcher feedFetcher = new FeedFetcher(feedParser, httpGetter); FeedRefreshUpdater feedUpdater = new FeedRefreshUpdater(sessionFactory, feedUpdateService, pubSubService, queues, config, metrics, feedSubscriptionDAO, cacheService); FeedRefreshWorker feedWorker = new FeedRefreshWorker(feedUpdater, feedFetcher, queues, config, metrics); FeedRefreshTaskGiver taskGiver = new FeedRefreshTaskGiver(sessionFactory, queues, feedDAO, feedWorker, config, metrics); - // TODO add caching of credentials somehow - environment.jersey().register(new SecurityCheckProvider(userService)); + // Auth/session management + environment.servlets().setSessionHandler(new SessionHandler()); + environment.jersey().register(new SecurityCheckUserServiceProvider(userService)); + environment.jersey().register(SecurityCheckProvider.class); + environment.jersey().register(HttpSessionProvider.class); + + // REST resources environment.jersey().setUrlPattern("/rest/*"); environment.jersey() .register(new AdminREST(userDAO, userRoleDAO, userService, encryptionService, cleaningService, config, metrics)); @@ -174,18 +188,20 @@ public class CommaFeedApplication extends Application { environment.jersey().register(new ServerREST(httpGetter, config, applicationPropertiesService)); environment.jersey().register(new UserREST(userDAO, userRoleDAO, userSettingsDAO, userService, encryptionService)); + // Servlets NextUnreadServlet nextUnreadServlet = new NextUnreadServlet(feedSubscriptionDAO, feedEntryStatusDAO, feedCategoryDAO, userService, config); + LogoutServlet logoutServlet = new LogoutServlet(config); environment.servlets().addServlet("next", nextUnreadServlet).addMapping("/next"); + environment.servlets().addServlet("logout", logoutServlet).addMapping("/logout"); + // Managed objects environment.lifecycle().manage(startupService); environment.lifecycle().manage(taskGiver); environment.lifecycle().manage(feedWorker); environment.lifecycle().manage(feedUpdater); - // TODO user registration - // TODO user login page - // TODO cookie support ? + // TODO user login + registration page // TODO translations } diff --git a/src/main/java/com/commafeed/backend/service/UserService.java b/src/main/java/com/commafeed/backend/service/UserService.java index e71592a8..83469bb5 100644 --- a/src/main/java/com/commafeed/backend/service/UserService.java +++ b/src/main/java/com/commafeed/backend/service/UserService.java @@ -17,6 +17,7 @@ import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserRole; import com.commafeed.backend.model.UserRole.Role; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; @RequiredArgsConstructor @@ -30,7 +31,7 @@ public class UserService { private final PasswordEncryptionService encryptionService; private final CommaFeedConfiguration config; - public User login(String name, String password) { + public Optional login(String name, String password) { if (name == null || password == null) { return null; } @@ -58,22 +59,22 @@ public class UserService { if (saveUser) { userDAO.saveOrUpdate(user); } - return user; + return Optional.fromNullable(user); } } - return null; + return Optional.absent(); } - public User login(String apiKey) { + public Optional login(String apiKey) { if (apiKey == null) { - return null; + return Optional.absent(); } User user = userDAO.findByApiKey(apiKey); if (user != null && !user.isDisabled()) { - return user; + return Optional.fromNullable(user); } - return null; + return Optional.absent(); } public User register(String name, String password, String email, Collection roles) { diff --git a/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java b/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java index 47402e27..8f7ea7b8 100644 --- a/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java +++ b/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java @@ -1,18 +1,23 @@ package com.commafeed.frontend.auth; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; 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.extern.slf4j.Slf4j; +import lombok.RequiredArgsConstructor; import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.StringUtil; +import com.commafeed.CommaFeedApplication; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.service.UserService; +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; @@ -20,66 +25,93 @@ 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; -@Slf4j public class SecurityCheckProvider implements InjectableProvider { - private static class SecurityCheckInjectable extends AbstractHttpContextInjectable { - private static final String PREFIX = "Basic"; + public static class SecurityCheckUserServiceProvider extends SingletonTypeInjectableProvider { - private final UserService userService; - private Role role; - private final boolean apiKeyAllowed; - - private SecurityCheckInjectable(UserService userService, Role role, boolean apiKeyAllowed) { - this.userService = userService; - this.role = role; - this.apiKeyAllowed = apiKeyAllowed; - } - - @Override - public User getValue(HttpContext c) { - final String header = c.getRequest().getHeaderValue(HttpHeaders.AUTHORIZATION); - try { - if (header != null) { - final int space = header.indexOf(' '); - if (space > 0) { - final String method = header.substring(0, space); - if (PREFIX.equalsIgnoreCase(method)) { - final String decoded = B64Code.decode(header.substring(space + 1), StringUtil.__ISO_8859_1); - final int i = decoded.indexOf(':'); - if (i > 0) { - final String username = decoded.substring(0, i); - final String password = decoded.substring(i + 1); - final User user = userService.login(username, password); - if (user != null && user.hasRole(role)) { - return user; - } - } - } - } - } else { - String apiKey = c.getUriInfo().getPathParameters().getFirst("apiKey"); - if (apiKey != null && apiKeyAllowed) { - User user = userService.login(apiKey); - if (user != null && user.hasRole(role)) { - return user; - } - } - } - } catch (IllegalArgumentException e) { - log.debug("Error decoding credentials", e); - } - - throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED) - .header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"CommaFeed\"") - .entity("Credentials are required to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build()); + public SecurityCheckUserServiceProvider(UserService userService) { + super(UserService.class, userService); } } + @RequiredArgsConstructor + private static class SecurityCheckInjectable extends AbstractHttpContextInjectable { + private static final String PREFIX = "Basic"; + + private final HttpServletRequest request; + private final UserService userService; + private final Role role; + private final boolean apiKeyAllowed; + + @Override + public User getValue(HttpContext c) { + Optional user = cookieSessionLogin(); + if (!user.isPresent()) { + user = basicAuthenticationLogin(c); + } + if (!user.isPresent()) { + user = apiKeyLogin(c); + } + + if (user.isPresent()) { + return user.get(); + } else { + throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED) + .entity("Credentials are required to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build()); + } + } + + private Optional cookieSessionLogin() { + HttpSession session = request.getSession(false); + if (session != null) { + User user = (User) session.getAttribute(CommaFeedApplication.SESSION_USER); + return Optional.fromNullable(user); + } + return Optional.absent(); + } + + 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); + Optional user = userService.login(username, password); + if (user.isPresent() && user.get().hasRole(role)) { + return user; + } + } + } + } + } + return Optional.absent(); + } + + private Optional apiKeyLogin(HttpContext c) { + String apiKey = c.getUriInfo().getPathParameters().getFirst("apiKey"); + if (apiKey != null && apiKeyAllowed) { + Optional user = userService.login(apiKey); + if (user.isPresent() && user.get().hasRole(role)) { + return user; + } + } + return Optional.absent(); + } + } + + private HttpServletRequest request; private UserService userService; - public SecurityCheckProvider(UserService userService) { + public SecurityCheckProvider(@Context HttpServletRequest request, @Context UserService userService) { + this.request = request; this.userService = userService; } @@ -90,6 +122,6 @@ public class SecurityCheckProvider implements InjectableProvider getInjectable(ComponentContext ic, SecurityCheck sc, Parameter c) { - return new SecurityCheckInjectable<>(userService, sc.value(), sc.apiKeyAllowed()); + return new SecurityCheckInjectable<>(request, userService, sc.value(), sc.apiKeyAllowed()); } } diff --git a/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java b/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java new file mode 100644 index 00000000..5584e498 --- /dev/null +++ b/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java @@ -0,0 +1,20 @@ +package com.commafeed.frontend.model.request; + +import java.io.Serializable; + +import lombok.Data; + +import com.wordnik.swagger.annotations.ApiModel; +import com.wordnik.swagger.annotations.ApiModelProperty; + +@SuppressWarnings("serial") +@Data +@ApiModel +public class LoginRequest implements Serializable { + + @ApiModelProperty(value = "username", required = true) + private String name; + + @ApiModelProperty(value = "password", required = true) + private String password; +} diff --git a/src/main/java/com/commafeed/frontend/resource/UserREST.java b/src/main/java/com/commafeed/frontend/resource/UserREST.java index eb22084d..f9856b05 100644 --- a/src/main/java/com/commafeed/frontend/resource/UserREST.java +++ b/src/main/java/com/commafeed/frontend/resource/UserREST.java @@ -1,9 +1,11 @@ package com.commafeed.frontend.resource; import io.dropwizard.hibernate.UnitOfWork; +import io.dropwizard.jersey.sessions.Session; import java.util.Arrays; +import javax.servlet.http.HttpSession; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -33,8 +35,10 @@ import com.commafeed.backend.service.UserService; import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.Settings; import com.commafeed.frontend.model.UserModel; +import com.commafeed.frontend.model.request.LoginRequest; import com.commafeed.frontend.model.request.ProfileModificationRequest; import com.commafeed.frontend.model.request.RegistrationRequest; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; @@ -203,7 +207,20 @@ public class UserREST { } catch (Exception e) { return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build(); } + } + @Path("/login") + @POST + @UnitOfWork + @ApiOperation(value = "Login and create a session") + public Response login(@ApiParam(required = true) LoginRequest req, @Session HttpSession session) { + Optional user = userService.login(req.getName(), req.getPassword()); + if (user.isPresent()) { + session.setAttribute(CommaFeedApplication.SESSION_USER, user); + return Response.ok().build(); + } else { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } } @Path("/profile/deleteAccount") diff --git a/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java b/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java new file mode 100644 index 00000000..6500769d --- /dev/null +++ b/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java @@ -0,0 +1,25 @@ +package com.commafeed.frontend.servlet; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import lombok.RequiredArgsConstructor; + +import com.commafeed.CommaFeedConfiguration; + +@SuppressWarnings("serial") +@RequiredArgsConstructor +public class LogoutServlet extends HttpServlet { + + private final CommaFeedConfiguration config; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + req.getSession().invalidate(); + resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl())); + } +} diff --git a/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java b/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java index 37b755cc..0cd05390 100644 --- a/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java +++ b/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java @@ -24,6 +24,7 @@ import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.service.UserService; import com.commafeed.frontend.resource.CategoryREST; +import com.google.common.base.Optional; import com.google.common.collect.Iterables; @SuppressWarnings("serial") @@ -51,8 +52,8 @@ public class NextUnreadServlet extends HttpServlet { return; } - User user = userService.login(apiKey); - if (user == null) { + Optional user = userService.login(apiKey); + if (!user.isPresent()) { resp.getWriter().write("unknown user or api key not found"); return; } @@ -65,14 +66,15 @@ public class NextUnreadServlet extends HttpServlet { List statuses = null; if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) { - List subs = feedSubscriptionDAO.findAll(user); - statuses = feedEntryStatusDAO.findBySubscriptions(user, subs, true, null, null, 0, 1, order, true, false, null); + List subs = feedSubscriptionDAO.findAll(user.get()); + statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, order, true, false, null); } else { - FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId)); + FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId)); if (category != null) { - List children = feedCategoryDAO.findAllChildrenCategories(user, category); - List subscriptions = feedSubscriptionDAO.findByCategories(user, children); - statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, 1, order, true, false, null); + List children = feedCategoryDAO.findAllChildrenCategories(user.get(), category); + List subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children); + statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, null, 0, 1, order, true, false, + null); } }