diff --git a/src/main/java/com/commafeed/backend/dao/UserDAO.java b/src/main/java/com/commafeed/backend/dao/UserDAO.java index 48f22ef4..937546c8 100644 --- a/src/main/java/com/commafeed/backend/dao/UserDAO.java +++ b/src/main/java/com/commafeed/backend/dao/UserDAO.java @@ -29,4 +29,19 @@ public class UserDAO extends GenericDAO { return user; } + public User findByApiKey(String key) { + CriteriaQuery query = builder.createQuery(getType()); + Root root = query.from(getType()); + query.where(builder.equal(root.get(User_.apiKey), key)); + TypedQuery q = em.createQuery(query); + + User user = null; + try { + user = q.getSingleResult(); + } catch (NoResultException e) { + user = null; + } + return user; + } + } diff --git a/src/main/java/com/commafeed/backend/model/User.java b/src/main/java/com/commafeed/backend/model/User.java index 14efa899..8c7183a6 100644 --- a/src/main/java/com/commafeed/backend/model/User.java +++ b/src/main/java/com/commafeed/backend/model/User.java @@ -31,6 +31,9 @@ public class User extends AbstractModel { @Column(length = 256, nullable = false) private byte[] password; + @Column(length = 40, unique = true) + private String apiKey; + @Column(length = 8, nullable = false) private byte[] salt; @@ -99,4 +102,12 @@ public class User extends AbstractModel { this.lastLogin = lastLogin; } + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + } diff --git a/src/main/java/com/commafeed/frontend/CommaFeedSession.java b/src/main/java/com/commafeed/frontend/CommaFeedSession.java index 15675b5b..7e6ea9db 100644 --- a/src/main/java/com/commafeed/frontend/CommaFeedSession.java +++ b/src/main/java/com/commafeed/frontend/CommaFeedSession.java @@ -36,10 +36,6 @@ public class CommaFeedSession extends AuthenticatedWebSession { return user; } - public void setUser(User user) { - this.user = user; - } - public static CommaFeedSession get() { return (CommaFeedSession) Session.get(); } @@ -52,6 +48,11 @@ public class CommaFeedSession extends AuthenticatedWebSession { @Override public boolean authenticate(String userName, String password) { User user = userService.login(userName, password); + setUser(user); + return user != null; + } + + public void setUser(User user) { if (user == null) { this.user = null; this.roles = new Roles(); @@ -64,7 +65,6 @@ public class CommaFeedSession extends AuthenticatedWebSession { this.user = user; this.roles = new Roles(roleSet.toArray(new String[0])); } - return user != null; } } diff --git a/src/main/java/com/commafeed/frontend/SecurityCheck.java b/src/main/java/com/commafeed/frontend/SecurityCheck.java index 49965c19..e389008a 100644 --- a/src/main/java/com/commafeed/frontend/SecurityCheck.java +++ b/src/main/java/com/commafeed/frontend/SecurityCheck.java @@ -22,4 +22,7 @@ public @interface SecurityCheck { */ @Nonbinding Role value() default Role.USER; + + @Nonbinding + boolean apiKeyAllowed() default false; } \ No newline at end of file diff --git a/src/main/java/com/commafeed/frontend/model/UserModel.java b/src/main/java/com/commafeed/frontend/model/UserModel.java index 5fc82139..286ed86a 100644 --- a/src/main/java/com/commafeed/frontend/model/UserModel.java +++ b/src/main/java/com/commafeed/frontend/model/UserModel.java @@ -24,6 +24,9 @@ public class UserModel implements Serializable { @ApiProperty("user email, if any") private String email; + @ApiProperty("api key") + private String apiKey; + @ApiProperty(value = "user password, never returned by the api") private String password; @@ -81,4 +84,12 @@ public class UserModel implements Serializable { this.email = email; } + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + } diff --git a/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java b/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java index 9916f883..ec30b206 100644 --- a/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java +++ b/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java @@ -18,6 +18,9 @@ public class ProfileModificationRequest { @ApiProperty(value = "changes password of the user, if specified") private String password; + @ApiProperty(value = "generate a new api key") + private boolean newApiKey; + public String getEmail() { return email; } @@ -34,4 +37,12 @@ public class ProfileModificationRequest { this.password = password; } + public boolean isNewApiKey() { + return newApiKey; + } + + public void setNewApiKey(boolean newApiKey) { + this.newApiKey = newApiKey; + } + } diff --git a/src/main/java/com/commafeed/frontend/rest/resources/AbstractREST.java b/src/main/java/com/commafeed/frontend/rest/resources/AbstractREST.java index 9e7830be..429f217b 100644 --- a/src/main/java/com/commafeed/frontend/rest/resources/AbstractREST.java +++ b/src/main/java/com/commafeed/frontend/rest/resources/AbstractREST.java @@ -97,7 +97,7 @@ public abstract class AbstractREST { @Inject OPMLImporter opmlImporter; - + @Inject OPMLExporter opmlExporter; @@ -124,44 +124,61 @@ public abstract class AbstractREST { .fetchCreateAndSetSession(cycle); if (session.getUser() == null) { - IAuthenticationStrategy authenticationStrategy = app - .getSecuritySettings().getAuthenticationStrategy(); - String[] data = authenticationStrategy.load(); + cookieLogin(app, session); + } + if (session.getUser() == null) { + basicHttpLogin(swreq, session); + } + } + + private void cookieLogin(CommaFeedApplication app, CommaFeedSession session) { + IAuthenticationStrategy authenticationStrategy = app + .getSecuritySettings().getAuthenticationStrategy(); + String[] data = authenticationStrategy.load(); + if (data != null && data.length > 1) { + session.signIn(data[0], data[1]); + } + } + + private void basicHttpLogin(ServletWebRequest req, CommaFeedSession session) { + String value = req.getHeader(HttpHeaders.AUTHORIZATION); + if (value != null && value.startsWith("Basic ")) { + value = value.substring(6); + String decoded = new String(Base64.decodeBase64(value)); + String[] data = decoded.split(":"); if (data != null && data.length > 1) { session.signIn(data[0], data[1]); - } else { - String value = swreq.getHeader(HttpHeaders.AUTHORIZATION); - if (value != null && value.startsWith("Basic ")) { - value = value.substring(6); - String decoded = new String(Base64.decodeBase64(value)); - data = decoded.split(":"); - if (data != null && data.length > 1) { - session.signIn(data[0], data[1]); - } - } } } } + private void apiKeyLogin() { + String apiKey = request.getParameter("apiKey"); + User user = userDAO.findByApiKey(apiKey); + CommaFeedSession.get().setUser(user); + } + protected User getUser() { return CommaFeedSession.get().getUser(); } @AroundInvoke public Object checkSecurity(InvocationContext context) throws Exception { - User user = getUser(); - boolean allowed = true; + User user = null; Method method = context.getMethod(); + SecurityCheck check = method.isAnnotationPresent(SecurityCheck.class) ? method + .getAnnotation(SecurityCheck.class) : method + .getDeclaringClass().getAnnotation(SecurityCheck.class); - if (method.isAnnotationPresent(SecurityCheck.class)) { - allowed = checkRole(user, method.getAnnotation(SecurityCheck.class)); - } else if (method.getDeclaringClass().isAnnotationPresent( - SecurityCheck.class)) { - allowed = checkRole( - user, - method.getDeclaringClass().getAnnotation( - SecurityCheck.class)); + if (check != null) { + user = getUser(); + if (user == null && check.apiKeyAllowed()) { + apiKeyLogin(); + user = getUser(); + } + + allowed = checkRole(check.value()); } if (!allowed) { if (user == null) { @@ -181,8 +198,7 @@ public abstract class AbstractREST { return context.proceed(); } - private boolean checkRole(User user, SecurityCheck annotation) { - Role requiredRole = annotation.value(); + private boolean checkRole(Role requiredRole) { if (requiredRole == Role.NONE) { return true; } diff --git a/src/main/java/com/commafeed/frontend/rest/resources/CategoryREST.java b/src/main/java/com/commafeed/frontend/rest/resources/CategoryREST.java index 26076163..81ccec7f 100644 --- a/src/main/java/com/commafeed/frontend/rest/resources/CategoryREST.java +++ b/src/main/java/com/commafeed/frontend/rest/resources/CategoryREST.java @@ -26,7 +26,9 @@ import org.slf4j.LoggerFactory; import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedSubscription; +import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserSettings.ReadingOrder; +import com.commafeed.frontend.SecurityCheck; import com.commafeed.frontend.model.Category; import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Entry; @@ -111,6 +113,7 @@ public class CategoryREST extends AbstractResourceREST { @GET @ApiOperation(value = "Get category entries as feed", notes = "Get a feed of category entries") @Produces(MediaType.APPLICATION_XML) + @SecurityCheck(value = Role.USER, apiKeyAllowed = true) public String getCategoryEntriesAsFeed( @ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id) { diff --git a/src/main/java/com/commafeed/frontend/rest/resources/FeedREST.java b/src/main/java/com/commafeed/frontend/rest/resources/FeedREST.java index a7c46b01..e4237918 100644 --- a/src/main/java/com/commafeed/frontend/rest/resources/FeedREST.java +++ b/src/main/java/com/commafeed/frontend/rest/resources/FeedREST.java @@ -31,14 +31,16 @@ import com.commafeed.backend.feeds.FetchedFeed; import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedSubscription; +import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserSettings.ReadingOrder; +import com.commafeed.frontend.SecurityCheck; import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Entry; import com.commafeed.frontend.model.FeedInfo; import com.commafeed.frontend.model.Subscription; +import com.commafeed.frontend.model.request.FeedModificationRequest; import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.frontend.model.request.MarkRequest; -import com.commafeed.frontend.model.request.FeedModificationRequest; import com.commafeed.frontend.model.request.SubscribeRequest; import com.commafeed.frontend.rest.Enums.ReadType; import com.google.common.base.Preconditions; @@ -98,6 +100,7 @@ public class FeedREST extends AbstractResourceREST { @GET @ApiOperation(value = "Get feed entries as a feed", notes = "Get a feed of feed entries") @Produces(MediaType.APPLICATION_XML) + @SecurityCheck(value = Role.USER, apiKeyAllowed = true) public String getFeedEntriesAsFeed( @ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id) { diff --git a/src/main/java/com/commafeed/frontend/rest/resources/UserREST.java b/src/main/java/com/commafeed/frontend/rest/resources/UserREST.java index bec640ae..96b1bff8 100644 --- a/src/main/java/com/commafeed/frontend/rest/resources/UserREST.java +++ b/src/main/java/com/commafeed/frontend/rest/resources/UserREST.java @@ -1,11 +1,14 @@ package com.commafeed.frontend.rest.resources; +import java.util.UUID; + import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang.StringUtils; import com.commafeed.backend.StartupBean; @@ -78,6 +81,7 @@ public class UserREST extends AbstractResourceREST { userModel.setName(user.getName()); userModel.setEmail(user.getEmail()); userModel.setEnabled(!user.isDisabled()); + userModel.setApiKey(user.getApiKey()); for (UserRole role : userRoleDAO.findAll(user)) { if (role.getRole() == Role.ADMIN) { userModel.setAdmin(true); @@ -95,13 +99,24 @@ public class UserREST extends AbstractResourceREST { if (StartupBean.USERNAME_DEMO.equals(user.getName())) { return Response.status(Status.UNAUTHORIZED).build(); } + user.setEmail(request.getEmail()); if (StringUtils.isNotBlank(request.getPassword())) { byte[] password = encryptionService.getEncryptedPassword( request.getPassword(), user.getSalt()); user.setPassword(password); + user.setApiKey(generateKey(user)); + } + if (request.isNewApiKey()) { + user.setApiKey(generateKey(user)); } userDAO.update(user); return Response.ok().build(); } + + private String generateKey(User user) { + byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID() + .toString(), user.getSalt()); + return DigestUtils.sha1Hex(key); + } } diff --git a/src/main/webapp/js/controllers.js b/src/main/webapp/js/controllers.js index 3f2ab2a3..a69d52f7 100644 --- a/src/main/webapp/js/controllers.js +++ b/src/main/webapp/js/controllers.js @@ -184,10 +184,11 @@ function($scope, $timeout, $stateParams, $window, $location, $state, $route, Cat }); }]); -module.controller('FeedDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedService', 'CategoryService', '$dialog', - function($scope, $state, $stateParams, FeedService, CategoryService, $dialog) { +module.controller('FeedDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedService', 'CategoryService', 'ProfileService', '$dialog', + function($scope, $state, $stateParams, FeedService, CategoryService, ProfileService, $dialog) { $scope.CategoryService = CategoryService; + $scope.user = ProfileService.get(); $scope.sub = FeedService.get({ id : $stateParams._id @@ -250,9 +251,10 @@ module.controller('FeedDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedS }; }]); -module.controller('CategoryDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedService', 'CategoryService', '$dialog', - function($scope, $state, $stateParams, FeedService, CategoryService, $dialog) { +module.controller('CategoryDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedService', 'CategoryService', 'ProfileService', '$dialog', + function($scope, $state, $stateParams, FeedService, CategoryService, ProfileService, $dialog) { $scope.CategoryService = CategoryService; + $scope.user = ProfileService.get(); $scope.isMeta = function() { return parseInt($stateParams._id, 10) != $stateParams._id; @@ -264,7 +266,7 @@ module.controller('CategoryDetailsCtrl', ['$scope', '$state', '$stateParams', 'F return elem.id != $scope.category.id; }; - CategoryService.get(function() { + CategoryService.init(function() { if ($scope.isMeta()) { $scope.category = { id : $stateParams._id, @@ -785,7 +787,8 @@ function($scope, $location, ProfileService, AnalyticsService) { } var o = { email : $scope.user.email, - password : $scope.user.password + password : $scope.user.password, + newApiKey : $scope.newApiKey }; ProfileService.save(o, function() { diff --git a/src/main/webapp/templates/feeds.category_details.html b/src/main/webapp/templates/feeds.category_details.html index a8f83f48..7fa745c2 100644 --- a/src/main/webapp/templates/feeds.category_details.html +++ b/src/main/webapp/templates/feeds.category_details.html @@ -24,7 +24,8 @@
- {{'rest/category/entriesAsFeed?id=' + category.id}} + link + Generate an API key in your profile first.
diff --git a/src/main/webapp/templates/feeds.feed_details.html b/src/main/webapp/templates/feeds.feed_details.html index d6cfd977..d4a8c9be 100644 --- a/src/main/webapp/templates/feeds.feed_details.html +++ b/src/main/webapp/templates/feeds.feed_details.html @@ -37,7 +37,8 @@
- {{'rest/feed/entriesAsFeed?id=' + sub.id}} + link + Generate an API key in your profile first.
diff --git a/src/main/webapp/templates/profile.html b/src/main/webapp/templates/profile.html index fb5a2075..7f6861fc 100644 --- a/src/main/webapp/templates/profile.html +++ b/src/main/webapp/templates/profile.html @@ -31,6 +31,21 @@ Passwords do not match + +
+ +
+ {{user.apiKey}} + Not generated yet +
+
+
+ +
+ + Changing password will generate a new API key +
+